﻿using System;
using System.Windows.Input;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using ThreeDWorld;

namespace Demo
{
    public class Game1 : Game
    {
        SpriteFont _Font;
        VertexShader _LightVertex;
        PixelShader _LightPixel;
        VertexShader _DualTextureVertex;
        PixelShader _DualTexturePixel;
        Map _Map;
        KeyFrameMesh _PlayerMesh;
        Animation _PlayerAnimation;
        Texture2D _PlayerSkin;
        Orientation _PlayerOrientation;
        SoundEffectInstance _TriggerSound;
        float _Gravity;
        float _Y;
        float _UpForce;
        bool _Reset;
        bool _SDown;
        bool _OnGround;

        public Game1()
        {
            Load();
        }

        private void Load()
        {
            // Skybox is setup to use the sky shaders which must be included in the content build
            SkyBox.Texture = Content.Load<TextureCube>("CubeMaps/Sky1");

            // load font
            _Font = Content.Load<SpriteFont>("Fonts/fontcourier");

            // load shaders
            _LightVertex = Content.Load<VertexShader>("LightVertex");
            _LightPixel = Content.Load<PixelShader>("LightPixel");
            _DualTextureVertex = Content.Load<VertexShader>("DualTextureVertex");
            _DualTexturePixel = Content.Load<PixelShader>("DualTexturePixel");

            // map
            _Map = Content.Load<Map>("DeleD/Map1");

            // player mesh and animation
            _PlayerMesh = Content.Load<KeyFrameMesh>("Meshes/Smiley");
            _PlayerAnimation = new Animation();
            _PlayerAnimation.Sequence = new Sequence(0, 10, 6, true);
            _PlayerSkin = Content.Load<Texture2D>("Meshes/SmileySkin");

            // load sounds
            _TriggerSound = Content.Load<SoundEffect>("Sounds/Trigger").CreateInstance();

            // Reset game
            Reset();
        }

        protected override void Update(float elapsedTime, float totalTime)
        {
            if (_Reset)
            {
                // if trigger sound is done playing then reset
                if (_TriggerSound.State == SoundState.Stopped)
                {
                    Reset();
                }
            }
            else
            {
                Vector3 d;
                bool moving = false;

                // test for jumping
                if (Input.IsKeyDown(Key.S) && !_SDown && _OnGround)
                {
                    _UpForce = 600;
                    _SDown = true;
                }
                else if (!Input.IsKeyDown(Key.S))
                {
                    _SDown = false;
                }

                // test for changing direction
                if (Input.IsKeyDown(Key.J))
                {
                    _PlayerOrientation.Rotate(MathHelper.Pi * elapsedTime);
                    Camera.Rotate(0, MathHelper.Pi * elapsedTime);
                    moving = true;
                }
                else if (Input.IsKeyDown(Key.K))
                {
                    _PlayerOrientation.Rotate(-MathHelper.Pi * elapsedTime);
                    Camera.Rotate(0, -MathHelper.Pi * elapsedTime);
                    moving = true;
                }

                // test for moving forward
                if (Input.IsKeyDown(Key.A))
                {
                    _PlayerOrientation.Position += _PlayerOrientation.Forward * 300.0f * elapsedTime;
                    moving = true;
                }

                // update forces
                _Gravity = Math.Max(-5000, _Gravity - 100 + _UpForce);
                _UpForce = Math.Max(0, _UpForce - 100);

                if (_Gravity > 1)
                {
                    moving = true;
                }

                // set sequence to moving or idle
                if (moving)
                {
                    _PlayerAnimation.Sequence = new Sequence(11, 18, 9, true);
                }
                else
                {
                    _PlayerAnimation.Sequence = new Sequence(0, 10, 6, true);
                }

                // apply gravity
                _PlayerOrientation.Position += Vector3.UnitY * _Gravity * elapsedTime;

                // reset y if on ground
                if (_OnGround)
                {
                    _Y = _PlayerOrientation.Position.Y;
                }

                // initialize state for collision
                _OnGround = false;

                // resolve camera collision with map
                ResolveCameraCollision();

                // resolve player collision with map
                ResolvePlayerCollision();

                // make camera follow player on a leash
                d = Camera.Position - _PlayerOrientation.Position;
                if (d.Length() > 300)
                {
                    d = Vector3.Normalize(d) * (d.Length() - 300.0f * elapsedTime);
                    Camera.MoveTo(_PlayerOrientation.Position + d);
                }

                // make camera look at final player position
                Camera.LookAt(_PlayerOrientation.Position);

                // determine if we have fallen to far
                if (_PlayerOrientation.Position.Y - _Y < -1000)
                {
                    _Reset = true;
                    _TriggerSound.Play();
                }

                // update the player key frame mesh animation;
                _PlayerAnimation.Update(_PlayerMesh, elapsedTime);
            }
        }

        protected override void Draw(float elapsedTime, float totalTime)
        {
            // camera matrices to pass to shaders
            Matrix view = Camera.View;
            Matrix projection = Camera.Projection;

            // player world matrices to pass to shaders (the inverse transpose is needed for lighting calculations)
            Matrix playerWorld = Matrix.CreateScale(2, 2, 2) * _PlayerOrientation.World;
            Matrix playerWorldInverseTranspose = Matrix.Transpose(Matrix.Invert(playerWorld));

            // lighting info to pass to shaders
            Vector3 lightPosition = _PlayerOrientation.Position + Vector3.UnitY * 400;
            Vector3 ambientColor = new Vector3(0.25f, 0.25f, 0.25f);
            Vector3 diffuseColor = new Vector3(1, 1, 1);

            // text to draw
            string[] info = new string[] {
                "<<< Click to Activate >>>",
                "-------------------------",
                " ",
                "    A - Move Forward",
                "    J - Turn Left",
                "    K - Turn Right",
                "    S - Jump"
            };

            // initial location of first line of text
            Vector2 position = new Vector2(8, 8);

            GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1, 0);

            // draw sky box
            SkyBox.Draw();

            // draw mesh
            GraphicsDevice.SetVertexShader(_LightVertex);
            GraphicsDevice.SetPixelShader(_LightPixel);

            GraphicsDevice.Textures[0] = _PlayerSkin;

            GraphicsDevice.SetVertexShaderConstantFloat4<Matrix>(8, ref view);
            GraphicsDevice.SetVertexShaderConstantFloat4<Matrix>(12, ref projection);

            GraphicsDevice.SetPixelShaderConstantFloat4<Vector3>(0, ref lightPosition);
            GraphicsDevice.SetPixelShaderConstantFloat4<Vector3>(1, ref ambientColor);
            GraphicsDevice.SetPixelShaderConstantFloat4<Vector3>(2, ref diffuseColor);

            GraphicsDevice.SetVertexShaderConstantFloat4<Matrix>(0, ref playerWorld);
            GraphicsDevice.SetVertexShaderConstantFloat4<Matrix>(4, ref playerWorldInverseTranspose);

            _PlayerAnimation.Draw(_PlayerMesh);

            // draw map
            GraphicsDevice.SetVertexShader(_DualTextureVertex);
            GraphicsDevice.SetPixelShader(_DualTexturePixel);

            GraphicsDevice.SetVertexShaderConstantFloat4<Matrix>(0, ref view);
            GraphicsDevice.SetVertexShaderConstantFloat4<Matrix>(4, ref projection);

            for (int i = 0; i != _Map.BatchCount; i++)
            {
                _Map.DrawBatch(i);
            }

            // draw text
            SpriteBatch.Begin();

            foreach (string line in info)
            {
                SpriteBatch.DrawText(_Font, line, position, Vector3.Zero, 1);
                position.Y += _Font.MeasureText(line).Y;
            }

            SpriteBatch.End();
        }

        private void Reset()
        {
            // set initial transform for camera and player
            _PlayerOrientation = Orientation.Create();
            _PlayerOrientation.Position = new Vector3(-500, 200, 500);
            _PlayerOrientation.Forward = new Vector3(1, 0, -1);

            Camera.MoveTo(new Vector3(-800, 500, 800));
            Camera.LookAt(_PlayerOrientation.Position);

            // initialize player physics info
            _Gravity = 0;
            _Y = _PlayerOrientation.Position.Y;
            _UpForce = 0;

            // S is not down
            _SDown = false;

            // reset is complete
            _Reset = false;
        }

        private void ResolveCameraCollision()
        {
            // the half size dimension of the camera collision bounds
            Vector3 d = new Vector3(100, 100, 100);

            // collide with primitives in map
            foreach (Primitive primitive in _Map.Primitives)
            {
                BoundingBox a = new BoundingBox(Camera.Position - d, Camera.Position + d);
                BoundingBox b = primitive.Bounds;
                float direction, translation;
                int axis;

                if (BoundsHelper.Collide(a, b, out axis, out direction, out translation))
                {
                    // always appling a positive y translation will make the camera climb obstacles
                    Camera.MoveTo(Camera.Position + Vector3.UnitY * translation);
                }
            }
            Camera.LookAt(_PlayerOrientation.Position);
        }

        private void ResolvePlayerCollision()
        {
            // the half size dimension of the player collision bounds
            Vector3 d = new Vector3(25, 15, 25);

            // collide with primitives in map
            foreach (Primitive primitive in _Map.Primitives)
            {
                BoundingBox a = new BoundingBox(_PlayerOrientation.Position - d, _PlayerOrientation.Position + d);
                BoundingBox b = primitive.Bounds;
                float direction, translation;
                int axis;

                if (BoundsHelper.Collide(a, b, out axis, out direction, out translation))
                {
                    Vector3 position = _PlayerOrientation.Position;

                    // 0 is x axis, 1 is y axis, 2 is z axis
                    if (axis == 0)
                    {
                        position.X += direction * translation;
                    }
                    else if (axis == 1)
                    {
                        position.Y += direction * translation;
                        if (direction > 0)
                        {
                            // if the direction of translation is 1 then we are on the ground
                            _OnGround = true;
                        }
                        // zero y forces 
                        _UpForce = 0;
                        _Gravity = 0;
                    }
                    else
                    {
                        position.Z += direction * translation;
                    }
                    _PlayerOrientation.Position = position;
                }
            }
        }
    }
}
